import React, {useRef, useState, useEffect, useCallback} from 'react';
import Indicator from './indicator';
import Measurer from './measurer';
import useScroll from './useScroll';

import styles from './style.module.css';
      
const INDICATOR_BAR_HEIGHT = 60;
const INITIAL_ITEM_HEIGHT = 20;

const findNearest = (min, max, test) =>{
  while (min <= max) {
    const mid = min + Math.floor((max - min) / 2);
    const res = test(mid);
    
    if (res === 0) {
      return mid;
    } else if (res > 0) {
      min = mid + 1;
    } else if (res < 0) {
      max = mid - 1;
    } else {
      throw Error('findNearest: invalid return');
    }
  }

  return (min > 0) ? min - 1 : 0;
};

const params = {
  bounds: [],
  LIOM: -1, 
  LIOP: -1, 
  measuredHeightTotal: 0,
  itemsTop:0,
}

const Pologram = ({itemData=[], rowRenderer: Row, height:outerHeight, buffer=20, initialItemHeight=INITIAL_ITEM_HEIGHT, topWidgetRenderer}) => {

  const itemCount = itemData.length;

  const scrollViewRef = useRef(null);

  const {
    state: {offset: scrollOffset, delta: scrollOffsetDelta},
    resetState: resetTop
  } = useScroll(scrollViewRef);

  /**
   * # 名词解释
   * 
   * 载入渲染 (Loading Pass of Rendering, LPR)
   * 对于宽高未知的item，必须要将item先渲染出来，搬运到document tree
   * 中，才有可能通过DOM方法测量其bounding box。对于有了正确高度和位置的
   * item，则不再进行测量及后续的校正渲染。
   * 
   * 校正渲染 (Correcting Pass of Rendering, CPR)
   * 在LPR结束之后，测量了每个item的bounding box（我们主要关心height）
   * 然后在渲染之前计算出将要被显示出来的item的实际位置，CPR才是真正呈现
   * 出我们想要看到的东西。
   * 
   * 首次渲染（Debut Rendering）
   * 指一个元素第一次被渲染，经过LPR和CPR两pass之后，有了正确的高度和位置
   * 
   * 回访渲染（Revisit Rendering）
   * 指在首次渲染之后，经过上下滚动重遇到一个元素时，因其已经有了正确的高度
   * 和位置，故只有LPR
   * 
   * LIOM: Last Index Of Measured
   * 指的是最后一个被测量高度的元素的位置。我们假定只有少数item会不频繁地
   * resize，因此大部分的元素在首次渲染后高度不会再变化。
   * 
   * LIOP: Last Index Of Positioned
   * 指的是最后一个被重新归位的元素的位置。在CPR阶段会一次性更新所有未归位的
   * item并将LIOP设置到min(LIOM, bottom)。此外，resizing也会设置LIOP。
   */

  const [, correct] = useState({});

  /**
   * # refreshItem 刷新某个（些）元素
   * 
   * 这是Pologram向外暴露的唯一函数。通常一个React组件是通过改变输入（绑定）
   * 的属性来触发re-render的，但是当涉及到输入的是一个容器（数组/列表或类似概
   * 念，Pologram就是用来干这个的），同时我们需要通过Pologram对其进行增删
   * 或修改等in-place操作，去复制这个容器对象并不是个好主意。
   * 
   * 首先，如果这个容器是一个很大的数据结构的一部分，那么在用新对象代替原有对象
   * 时，也需要处理好和原对象的引用关系。
   * 
   * 其次，改变原始的数据对象会初始化整个Pologram，这个操作不仅意味着要重绘
   * 所有屏幕上显示的Item，还会清除掉之前所有缓存的计算高度，因此不用profiling
   * 也知道这是十分昂贵的操作。
   * 
   * 所以，这里提供一个refresh方法，可以在Row的实现中通过useContext来使用，
   * 能够较细粒度地控制某个或某些Item的刷新。
   */

  const refreshItem = useCallback((index) => {
    params.LIOP = index - 1;
    correct({});
  }, [])


  const calcOverallHeight = () => {
    const { measuredHeightTotal, itemsTop, LIOM } = params;
    return itemsTop + measuredHeightTotal + (itemCount - LIOM + 1) * initialItemHeight + 100
  };

  const getIndicatorPosition = () => {

    const overallHeight = calcOverallHeight();
    return scrollOffset / (overallHeight) * (outerHeight - INDICATOR_BAR_HEIGHT)
  }


  
  const updateHeight = useCallback(({currentHeight, previousHeight, resized, index}) => {

    if (currentHeight === previousHeight) return;

    const {LIOP, LIOM} = params;

    Object.assign(params.bounds[index], {height: currentHeight});
    params.measuredHeightTotal += currentHeight - previousHeight;

    if (resized) {
      params.LIOP = Math.min(LIOP, index - 1);
      correct({});
    } else {
      params.LIOM = Math.max(LIOM, index);
    }
  }, []);



  const updatePosition = (head, buffer) => {

    const {LIOP, LIOM, itemsTop, bounds} = params;

    let curr = head;
    let cumulativeHeight = bounds[head]?.top ?? 0;

    const updateBound = (curr) => {

      if (curr > LIOM) {
        params.bounds[curr] ?? (params.bounds[curr] = {
          height: initialItemHeight,
          position: 'absolute'
        });
      }

      const last = curr - 1;

      if (curr > LIOP) {

        const top    = bounds[last]?.bottom ?? itemsTop;
        const bottom = bounds[curr].height + top;

        Object.assign(params.bounds[curr], {top, bottom});
      }

      cumulativeHeight += params.bounds[curr].height;
    }

    while (curr < itemCount && cumulativeHeight < scrollOffset + outerHeight) {
      updateBound(curr);
      curr ++;
    }

    let tailNormal = curr;

    let tailBuffer = Math.min(itemCount, tailNormal + buffer);

    while (curr < tailBuffer) {
      updateBound(curr);
      curr ++;
    }

    params.LIOP = Math.min(curr - 1, LIOM);

    return {
      tailNormal,
      tailBuffer,
    };
  }



  const getHeadIndex = () => {

    const {LIOM, bounds} = params;

    const headNormal = findNearest(0, LIOM, index => scrollOffset - bounds[index].top);
    const headBuffer = Math.max(0, headNormal - (scrollOffsetDelta < 0 ? buffer : 10));

    return {
      headNormal,
      headBuffer
    }
  }



  const getTailIndex = (head)=> {

    const {tailNormal, tailBuffer} = updatePosition(head, scrollOffsetDelta > 0 ? buffer : 10);

    return {
      tailNormal,
      tailBuffer
    }
  }

  const renderItems = () => {
    
    const {bounds} = params;

    if (itemData.length > 0){
      const {headBuffer, headNormal} = getHeadIndex();
      const {tailBuffer} = getTailIndex(headNormal);

      const items = [];
      for (let index = headBuffer; index < tailBuffer; index++) {

        items.push(<Measurer key={index} currentHeight={bounds[index].height} updateHeight={updateHeight} index={index}>
          <Row index={index} data={itemData} style={{...bounds[index]}} refreshItem={refreshItem}/>
        </Measurer>)
      }

      return items;
    } else {
      return [];
    }
  };

  useEffect(() => {
    params.LIOP = -1;
    params.LIOM = -1;
    resetTop();
    correct({});
  }, [itemData])

  const innerStyle = {
    position:'relative',
    height: calcOverallHeight()
  }

  return <div id="scrollViewIndicator" className={styles.indicator} style={{height: outerHeight}}>
      <div id="scrollView" ref={scrollViewRef} className={styles.outer}>
      <div className={styles['widget-top']}>
        {topWidgetRenderer}
      </div>
      <div style={innerStyle} >{renderItems()} </div>
    </div>
    <Indicator {...{
      barHeight: INDICATOR_BAR_HEIGHT,
      offset: getIndicatorPosition(),
      scrollOffsetDelta
    }}  />
  </div>

}

export default Pologram;